from google.appengine.ext import db

from model import validation



#### Constants ####

LIMITS = {
    "name_min_length":  1,
    "name_max_length":  25,
    # Matches a non-empty string of word characters (letters, digits and '_')
    "name_valid_regex": "^(\w)*$"
}



#### Exceptions ####

class NameNotUnique(validation.PropertyNotUnique):
    def __str__(self):
        return "Room with name \"" + self.value + "\" already exists"


class NameInvalidLength(validation.StringInvalidLength):
    pass


class NameInvalidFormat(validation.StringInvalidFormat):    
    def __str__(self):
        return "Only alphanumeric characters allowed"


class RoomIsFull(Exception):
    """Raised when adding a user to a room that reached it's user limit."""
    
    def __str__(self):
        return "Users limit (%d) reached" % Room._MAX_USERS


class UserNotInRoom(Exception):
    """Raised when operation on room invokes a user that's not in the room."""
    
    def __init__(self, username):
        self.username = username
    
    def __str__(self):
        return "User %s not in the room" % self.username


class UserAlreadyInRoom(Exception):
    """Raised when adding a user that is already in the room."""
    
    def __init__(self, username):
        self.username = username
    
    def __str__(self):
        return "User %s already in the room" % self.username



#### Model ####

class Room(db.Model):
    """
    Chat room. Users in the same room can talk to each other.
    
    Before creating instance of this class, you are obliged to check for name
    availability.
    
    Creating instance of this class throws exceptions:
    NameInvalidLength, NameInvalidFormat.
    
    Implemented in a way so that maximum number of users can easily be changed.
    
    """
    
    # Limit to maximum number of users in the room.
    _MAX_USERS = 2
    
    _name_validator = validation.combine_validators(
        validation.string_length_validator(
            LIMITS["name_min_length"],
            LIMITS["name_max_length"],
            NameInvalidLength
        ),
        validation.string_format_validator(
            LIMITS["name_valid_regex"],
            NameInvalidFormat
        )
    )
    
    name = db.StringProperty(required=True, validator=_name_validator)
    # Stores keys of all the users in the room.
    users = db.ListProperty(db.Key)
    # Stores connection status for all the users in the room. Positions on the
    # list correspond to the positions from users list.
    connected = db.ListProperty(bool)
    expired = db.BooleanProperty(default=False)
    
    @classmethod
    def check_name_uniqueness(class_obj, name):
        """
        Name's uniqueness must be checked before creating room with that name.
        Throws NameNotUnique.
        
        """
        query = class_obj.all().filter("name", name).filter("expired", False)
        if (query.get()):
            raise NameNotUnique(name)

    def __str__(self):
        result = 'Room ' + self.name + ', ' + str(self.occupancy()) + ' users'
        return result

    def occupancy(self):
        return len(self.users)
    
    def is_empty(self):
        return self.occupancy() == 0
    
    def is_full(self):
        return self.occupancy() >= self._MAX_USERS

    def add_user(self, user):
        """
        Throws RoomIsFull when user limit has been reached.
        Throws UserAlreadyInRoom when adding a user that is already in the room.
        
        """
        if self.has_user(user):
            raise UserAlreadyInRoom(user.username)
        if self.is_full():
            raise RoomIsFull()
        self.users.append(user.key())
        self.connected.append(False)

    def remove_user(self, user):
        """Throws UserNotInRoom when user is not in the room."""
        try:
            i = self.users.index(user.key())
            self.users.pop(i)
            self.connected.pop(i)
        except ValueError:
            raise UserNotInRoom(user.username)
    
    def has_user(self, user):
        return self.users.count(user.key()) > 0
        
    def get_other_users(self, this_user):
        """
        Returns the list with keys of all the connected users in the room,
        excluding this_user.
        
        Throws UserNotInRoom when this_user is not in the room.
        """
        try:
            # Make a copy of the list.
            result = list(self.users)
            
            # Remove unconnected users.
            this_i = result.index(this_user.key())
            for i, _ in enumerate(result):
                if (i != this_i) and (self.connected[i] == False):
                    result.pop(i)
            
            # Remove this_user.
            result.pop(result.index(this_user.key()))
            return result
        except ValueError:
            raise UserNotInRoom(this_user.username)
    
    def set_user_connection(self, user):
        """Throws UserNotInRoom when user is not in the room."""
        try:
            i = self.users.index(user.key())
            self.connected[i] = True
        except ValueError:
            raise UserNotInRoom(user.username)

    def is_user_connected(self, user):
        """Throws UserNotInRoom when user is not in the room."""
        try:
            i = self.users.index(user.key())
            return self.connected[i]
        except ValueError:
            raise UserNotInRoom(user.username)
    
    def set_expired(self):
        self.expired = True